// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "net/sdch/sdch_owner.h"

#include <utility>

#include "base/location.h"
#include "base/macros.h"
#include "base/memory/memory_pressure_listener.h"
#include "base/memory/ptr_util.h"
#include "base/run_loop.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/stringprintf.h"
#include "base/test/histogram_tester.h"
#include "base/test/simple_test_clock.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/values.h"
#include "net/base/sdch_manager.h"
#include "net/log/net_log.h"
#include "net/url_request/url_request.h"
#include "net/url_request/url_request_context.h"
#include "net/url_request/url_request_error_job.h"
#include "net/url_request/url_request_job.h"
#include "net/url_request/url_request_job_factory.h"
#include "net/url_request/url_request_test_util.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace net {

namespace {

    bool GetDictionaryForURL(SdchOwner::PrefStorage* store,
        const GURL& url,
        std::string* hash,
        base::DictionaryValue** dict)
    {
        base::DictionaryValue* sdch_dict = nullptr;
        if (!store->GetMutableValue(&sdch_dict))
            return false;

        base::DictionaryValue* dicts_dict = nullptr;
        if (!sdch_dict->GetDictionary("dictionaries", &dicts_dict))
            return false;

        base::DictionaryValue::Iterator it(*dicts_dict);
        while (!it.IsAtEnd()) {
            const base::DictionaryValue* d = nullptr;
            if (!it.value().GetAsDictionary(&d))
                continue;
            std::string dict_url;
            if (d->GetString("url", &dict_url) && dict_url == url.spec()) {
                if (hash)
                    *hash = it.key();
                if (dict)
                    dicts_dict->GetDictionary(it.key(), dict);
                return true;
            }
            it.Advance();
        }

        return false;
    }

    // This class supports copying so we can emulate persistent storage.
    class TestPrefStorage : public SdchOwner::PrefStorage {
    public:
        explicit TestPrefStorage(bool initialized)
            : initialized_(initialized)
            , initialization_observer_(nullptr)
        {
        }
        explicit TestPrefStorage(const TestPrefStorage& other)
            : initialized_(other.initialized_)
            , initialization_observer_(nullptr)
        { // Don't copy observer.
            storage_.MergeDictionary(&other.storage_);
        }

        ~TestPrefStorage() override { }

        void SetInitialized()
        {
            DCHECK(!initialized_);
            initialized_ = true;
            if (initialization_observer_)
                initialization_observer_->OnPrefStorageInitializationComplete(true);
        }

        ReadError GetReadError() const override { return PERSISTENCE_FAILURE_NONE; }

        bool GetValue(const base::DictionaryValue** result) const override
        {
            *result = &storage_;
            return true;
        }
        bool GetMutableValue(base::DictionaryValue** result) override
        {
            *result = &storage_;
            return true;
        }
        void SetValue(std::unique_ptr<base::DictionaryValue> value) override
        {
            storage_.Clear();
            storage_.MergeDictionary(value.get());
        }

        void ReportValueChanged() override { }

        // This storage class requires no special initialization.
        bool IsInitializationComplete() override { return initialized_; }
        void StartObservingInit(SdchOwner* observer) override
        {
            DCHECK(!initialization_observer_);
            initialization_observer_ = observer;
        }
        void StopObservingInit() override { initialization_observer_ = nullptr; }

    private:
        bool initialized_;
        SdchOwner* initialization_observer_;

        base::DictionaryValue storage_;
    };

} // namespace

static const char generic_url[] = "http://www.example.com";
static const char generic_domain[] = "www.example.com";

static std::string NewSdchDictionary(size_t dictionary_size)
{
    std::string dictionary;
    dictionary.append("Domain: ");
    dictionary.append(generic_domain);
    dictionary.append("\n");
    dictionary.append("\n");

    size_t original_dictionary_size = dictionary.size();
    dictionary.resize(dictionary_size);
    for (size_t i = original_dictionary_size; i < dictionary_size; ++i)
        dictionary[i] = static_cast<char>((i % 127) + 1);

    return dictionary;
}

int outstanding_url_request_error_counting_jobs = 0;
base::Closure* empty_url_request_jobs_callback = 0;

// Variation of URLRequestErrorJob to count number of outstanding
// instances and notify when that goes to zero.
class URLRequestErrorCountingJob : public URLRequestJob {
public:
    URLRequestErrorCountingJob(URLRequest* request,
        NetworkDelegate* network_delegate,
        int error)
        : URLRequestJob(request, network_delegate)
        , error_(error)
        , weak_factory_(this)
    {
        ++outstanding_url_request_error_counting_jobs;
    }

    void Start() override
    {
        base::ThreadTaskRunnerHandle::Get()->PostTask(
            FROM_HERE, base::Bind(&URLRequestErrorCountingJob::StartAsync, weak_factory_.GetWeakPtr()));
    }

private:
    ~URLRequestErrorCountingJob() override
    {
        --outstanding_url_request_error_counting_jobs;
        if (0 == outstanding_url_request_error_counting_jobs && empty_url_request_jobs_callback) {
            empty_url_request_jobs_callback->Run();
        }
    }

    void StartAsync()
    {
        NotifyStartError(URLRequestStatus(URLRequestStatus::FAILED, error_));
    }

    int error_;

    base::WeakPtrFactory<URLRequestErrorCountingJob> weak_factory_;

    DISALLOW_COPY_AND_ASSIGN(URLRequestErrorCountingJob);
};

static int error_jobs_created = 0;

class MockURLRequestJobFactory : public URLRequestJobFactory {
public:
    MockURLRequestJobFactory() { }

    ~MockURLRequestJobFactory() override { }

    URLRequestJob* MaybeCreateJobWithProtocolHandler(
        const std::string& scheme,
        URLRequest* request,
        NetworkDelegate* network_delegate) const override
    {
        ++error_jobs_created;
        return new URLRequestErrorCountingJob(request, network_delegate,
            ERR_INTERNET_DISCONNECTED);
    }

    URLRequestJob* MaybeInterceptRedirect(URLRequest* request,
        NetworkDelegate* network_delegate,
        const GURL& location) const override
    {
        return nullptr;
    }

    URLRequestJob* MaybeInterceptResponse(
        URLRequest* request,
        NetworkDelegate* network_delegate) const override
    {
        return nullptr;
    }

    bool IsHandledProtocol(const std::string& scheme) const override
    {
        return scheme == "http";
    };

    bool IsHandledURL(const GURL& url) const override
    {
        return url.SchemeIs("http");
    }

    bool IsSafeRedirectTarget(const GURL& location) const override
    {
        return false;
    }
};

class MockSdchDictionaryFetcher : public SdchDictionaryFetcher {
public:
    MockSdchDictionaryFetcher()
        : SdchDictionaryFetcher(&test_context_)
    {
    }
    ~MockSdchDictionaryFetcher() { }

    struct PendingRequest {
        PendingRequest(const GURL& url,
            const OnDictionaryFetchedCallback& callback)
            : url_(url)
            , callback_(callback)
        {
        }
        GURL url_;
        OnDictionaryFetchedCallback callback_;
    };

    virtual bool Schedule(const GURL& dictionary_url,
        const OnDictionaryFetchedCallback& callback)
    {
        if (!HasPendingRequest(dictionary_url)) {
            requests_.push_back(PendingRequest(dictionary_url, callback));
            return true;
        }
        return false;
    }

    virtual bool ScheduleReload(const GURL& dictionary_url,
        const OnDictionaryFetchedCallback& callback)
    {
        if (!HasPendingRequest(dictionary_url)) {
            requests_.push_back(PendingRequest(dictionary_url, callback));
            return true;
        }
        return false;
    }

    virtual void Cancel()
    {
        requests_.clear();
    }

    bool HasPendingRequest(const GURL& dictionary_url)
    {
        for (std::vector<PendingRequest>::iterator it = requests_.begin();
             it != requests_.end(); ++it) {
            if (it->url_ == dictionary_url)
                return true;
        }
        return false;
    }

    bool CompletePendingRequest(const GURL& dictionary_url,
        const std::string& dictionary_text,
        const BoundNetLog& net_log,
        bool was_from_cache)
    {
        for (std::vector<PendingRequest>::iterator it = requests_.begin();
             it != requests_.end(); ++it) {
            if (it->url_ == dictionary_url) {
                it->callback_.Run(dictionary_text, dictionary_url, net_log,
                    was_from_cache);
                requests_.erase(it);
                return true;
            }
        }
        return false;
    }

private:
    TestURLRequestContext test_context_;
    std::vector<PendingRequest> requests_;
    DISALLOW_COPY_AND_ASSIGN(MockSdchDictionaryFetcher);
};

// File testing infrastructure summary:
// * NewSdchDictionary(): Creates a dictionary of a specific size.
// * URLRequestErrorCountingJob: A URLRequestJob that returns an error
//   and counts the number of outstanding (started but not finished)
//   jobs, and calls a global callback when that number transitions to zero.
// * MockURLRequestJobFactory: Factory to create the above jobs.  Tracks
//   the number of jobs created.
// * SdchOwnerTest: Interfaces
//      * Access manager, owner, and net log
//      * Return the number of jobs created in a time interval
//      * Return dictionary present in the manager
//      * Notify SdchOwner of an incoming dictionary (& wait until jobs clear)
//      * Attempt to add a dictionary and test for success.
// Test patterns:
//      * Let the owner know about a Get-Dictionary header and test for
//        appropriate jobs being created.
//      * Let the owner know that a dictionary was successfully fetched
//        and test for appropriate outcome.
//      * Either of the above, having previously added dictionaries to create
//        a particular initial state.
class SdchOwnerTest : public testing::Test {
public:
    static const size_t kMaxSizeForTesting = 1000 * 50;
    static const size_t kMinFetchSpaceForTesting = 500;

    SdchOwnerTest()
        : last_jobs_created_(error_jobs_created)
        , dictionary_creation_index_(0)
        , sdch_owner_(new SdchOwner(&sdch_manager_, &url_request_context_))
    {
        // Any jobs created on this context will immediately error,
        // which leaves the test in control of signals to SdchOwner.
        url_request_context_.set_job_factory(&job_factory_);

        // Reduce sizes to reduce time for string operations.
        sdch_owner_->SetMaxTotalDictionarySize(kMaxSizeForTesting);
        sdch_owner_->SetMinSpaceForDictionaryFetch(kMinFetchSpaceForTesting);
    }

    SdchManager& sdch_manager() { return sdch_manager_; }
    SdchOwner& sdch_owner() { return *(sdch_owner_.get()); }
    BoundNetLog& bound_net_log() { return net_log_; }

    int JobsRecentlyCreated()
    {
        int result = error_jobs_created - last_jobs_created_;
        last_jobs_created_ = error_jobs_created;
        return result;
    }

    bool DictionaryPresentInManager(const std::string& server_hash)
    {
        // Presumes all tests use generic url.
        SdchProblemCode tmp;
        std::unique_ptr<SdchManager::DictionarySet> set(
            sdch_manager_.GetDictionarySetByHash(GURL(generic_url), server_hash,
                &tmp));
        return !!set.get();
    }

    void WaitForNoJobs()
    {
        if (outstanding_url_request_error_counting_jobs == 0)
            return;

        base::RunLoop run_loop;
        base::Closure quit_closure(run_loop.QuitClosure());
        empty_url_request_jobs_callback = &quit_closure;
        run_loop.Run();
        empty_url_request_jobs_callback = NULL;
    }

    void SignalGetDictionaryAndClearJobs(GURL request_url, GURL dictionary_url)
    {
        sdch_owner().OnGetDictionary(request_url, dictionary_url);
        WaitForNoJobs();
    }

    // Create a unique (by hash) dictionary of the given size,
    // associate it with a unique URL, add it to the manager through
    // SdchOwner::OnDictionaryFetched(), and return whether that
    // addition was successful or not.
    bool CreateAndAddDictionary(size_t size,
        base::Time last_used_time,
        base::Time created_time,
        std::string* server_hash_p)
    {
        GURL dictionary_url(
            base::StringPrintf("%s/d%d", generic_url, dictionary_creation_index_));
        std::string dictionary_text(NewSdchDictionary(size - 4));
        dictionary_text += base::StringPrintf("%04d", dictionary_creation_index_);
        ++dictionary_creation_index_;
        std::string client_hash;
        std::string server_hash;
        SdchManager::GenerateHash(dictionary_text, &client_hash, &server_hash);

        if (DictionaryPresentInManager(server_hash))
            return false;
        sdch_owner().OnDictionaryFetched(last_used_time, created_time, 0,
            dictionary_text, dictionary_url, net_log_,
            false);
        if (server_hash_p)
            *server_hash_p = server_hash;
        return DictionaryPresentInManager(server_hash);
    }

    bool CreateAndAddDictionary(size_t size,
        base::Time last_used_time,
        std::string* server_hash_p)
    {
        return CreateAndAddDictionary(size, last_used_time, base::Time(),
            server_hash_p);
    }

    void ResetOwner()
    {
        sdch_owner_.reset(new SdchOwner(&sdch_manager_, &url_request_context_));
    }

private:
    int last_jobs_created_;
    BoundNetLog net_log_;
    int dictionary_creation_index_;

    // The dependencies of these objects (sdch_owner_ -> {sdch_manager_,
    // url_request_context_}, url_request_context_->job_factory_) require
    // this order for correct destruction semantics.
    MockURLRequestJobFactory job_factory_;
    URLRequestContext url_request_context_;
    SdchManager sdch_manager_;
    std::unique_ptr<SdchOwner> sdch_owner_;

    DISALLOW_COPY_AND_ASSIGN(SdchOwnerTest);
};

// Does OnGetDictionary result in a fetch when there's enough space, and not
// when there's not?
TEST_F(SdchOwnerTest, OnGetDictionary_Fetching)
{
    GURL request_url(std::string(generic_url) + "/r1");

    // Fetch generated when empty.
    GURL dict_url1(std::string(generic_url) + "/d1");
    EXPECT_EQ(0, JobsRecentlyCreated());
    SignalGetDictionaryAndClearJobs(request_url, dict_url1);
    EXPECT_EQ(1, JobsRecentlyCreated());

    // Fetch generated when half full.
    GURL dict_url2(std::string(generic_url) + "/d2");
    std::string dictionary1(NewSdchDictionary(kMaxSizeForTesting / 2));
    sdch_owner().OnDictionaryFetched(base::Time::Now(), base::Time::Now(), 1,
        dictionary1, dict_url1, bound_net_log(),
        false);
    EXPECT_EQ(0, JobsRecentlyCreated());
    SignalGetDictionaryAndClearJobs(request_url, dict_url2);
    EXPECT_EQ(1, JobsRecentlyCreated());

    // Fetch not generated when close to completely full.
    GURL dict_url3(std::string(generic_url) + "/d3");
    std::string dictionary2(NewSdchDictionary(
        (kMaxSizeForTesting / 2 - kMinFetchSpaceForTesting / 2)));
    sdch_owner().OnDictionaryFetched(base::Time::Now(), base::Time::Now(), 1,
        dictionary2, dict_url2, bound_net_log(),
        false);
    EXPECT_EQ(0, JobsRecentlyCreated());
    SignalGetDictionaryAndClearJobs(request_url, dict_url3);
    EXPECT_EQ(0, JobsRecentlyCreated());
}

// Make sure attempts to add dictionaries do what they should.
TEST_F(SdchOwnerTest, OnDictionaryFetched_Fetching)
{
    GURL request_url(std::string(generic_url) + "/r1");
    std::string client_hash;
    std::string server_hash;

    // In the past, but still fresh for an unused dictionary.
    base::Time dictionary_last_used_time(base::Time::Now() - base::TimeDelta::FromMinutes(30));

    // Add successful when empty.
    EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 2,
        dictionary_last_used_time, nullptr));
    EXPECT_EQ(0, JobsRecentlyCreated());

    // Add successful when half full.
    EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 2,
        dictionary_last_used_time, nullptr));
    EXPECT_EQ(0, JobsRecentlyCreated());

    // Add unsuccessful when full.
    EXPECT_FALSE(CreateAndAddDictionary(kMaxSizeForTesting / 2,
        dictionary_last_used_time, nullptr));
    EXPECT_EQ(0, JobsRecentlyCreated());
}

// Confirm auto-eviction happens if space is needed.
TEST_F(SdchOwnerTest, ConfirmAutoEviction)
{
    base::Time start_time = base::Time::Now();
    std::string server_hash_d1;
    std::string server_hash_d2;
    std::string server_hash_d3;

    base::SimpleTestClock* test_clock = new base::SimpleTestClock();
    sdch_owner().SetClockForTesting(base::WrapUnique(test_clock));
    test_clock->SetNow(base::Time::Now());

    // Add two dictionaries, one recent, one more than a day in the past.
    base::Time fresh(base::Time::Now() - base::TimeDelta::FromHours(23));
    base::Time stale(base::Time::Now() - base::TimeDelta::FromHours(25));

    EXPECT_TRUE(
        CreateAndAddDictionary(kMaxSizeForTesting / 2, fresh, &server_hash_d1));
    EXPECT_TRUE(
        CreateAndAddDictionary(kMaxSizeForTesting / 2, stale, &server_hash_d2));

    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1));
    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d2));

    base::HistogramTester tester;
    const base::TimeDelta synthetic_delta = base::TimeDelta::FromSeconds(5);

    test_clock->Advance(synthetic_delta);

    EXPECT_TRUE(
        CreateAndAddDictionary(kMaxSizeForTesting / 2, fresh, &server_hash_d3));
    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1));
    EXPECT_FALSE(DictionaryPresentInManager(server_hash_d2));
    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d3));

    base::TimeDelta expected_proc_lifetime = synthetic_delta * 3 + base::Time::Now() - start_time;
    size_t expected_value_base = ((kMaxSizeForTesting / 2) * synthetic_delta.InMilliseconds()) / expected_proc_lifetime.InMilliseconds();

    const char* kHistogram = "Sdch3.TimeWeightedMemoryUse";
    tester.ExpectTotalCount(kHistogram, 0);

    // Dictionary insertions and deletions:
    // T = 0: insert d1 and d2
    // T = 5: insert d3, which evicts d2
    // T = 15: destroy SdchOwner, which evicts d1 and d3
    // Therefore, d2's lifetime is synthetic_delta, d1's is synthetic_delta * 3,
    // and d3's is synthetic_delta * 2. The expected_value_base variable is the
    // base factor for d2's memory consumption, of which d1's and d3's are
    // multiples.
    test_clock->Advance(synthetic_delta * 2);
    ResetOwner();

    tester.ExpectTotalCount(kHistogram, 3);
    tester.ExpectBucketCount(kHistogram, expected_value_base, 1);
    tester.ExpectBucketCount(kHistogram, expected_value_base * 2, 1);
    tester.ExpectBucketCount(kHistogram, expected_value_base * 3, 1);
}

// Confirm auto-eviction happens if space is needed, with a more complicated
// situation
TEST_F(SdchOwnerTest, ConfirmAutoEviction_2)
{
    std::string server_hash_d1;
    std::string server_hash_d2;
    std::string server_hash_d3;

    // Add dictionaries, one recent, two more than a day in the past that
    // between them add up to the space needed.
    base::Time fresh(base::Time::Now() - base::TimeDelta::FromHours(23));
    base::Time stale(base::Time::Now() - base::TimeDelta::FromHours(25));
    EXPECT_TRUE(
        CreateAndAddDictionary(kMaxSizeForTesting / 2, fresh, &server_hash_d1));

    EXPECT_TRUE(
        CreateAndAddDictionary(kMaxSizeForTesting / 4, stale, &server_hash_d2));
    EXPECT_TRUE(
        CreateAndAddDictionary(kMaxSizeForTesting / 4, stale, &server_hash_d3));

    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1));
    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d2));
    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d3));

    std::string server_hash_d4;
    EXPECT_TRUE(
        CreateAndAddDictionary(kMaxSizeForTesting / 2, fresh, &server_hash_d4));
    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1));
    EXPECT_FALSE(DictionaryPresentInManager(server_hash_d2));
    EXPECT_FALSE(DictionaryPresentInManager(server_hash_d3));
    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d4));
}

// Confirm if only one dictionary needs to be evicted it's the oldest.
TEST_F(SdchOwnerTest, ConfirmAutoEviction_Oldest)
{
    std::string server_hash_d1;
    std::string server_hash_d2;
    std::string server_hash_d3;

    // Add dictionaries, one recent, one two days in the past, and one
    // four days in the past.
    base::Time fresh(base::Time::Now() - base::TimeDelta::FromHours(23));
    base::Time stale_newer(base::Time::Now() - base::TimeDelta::FromHours(47));
    base::Time stale_older(base::Time::Now() - base::TimeDelta::FromHours(71));

    EXPECT_TRUE(
        CreateAndAddDictionary(kMaxSizeForTesting / 4, fresh, &server_hash_d1));

    EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 4, stale_newer,
        &server_hash_d2));

    EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 4, stale_older,
        &server_hash_d3));

    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1));
    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d2));
    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d3));

    // The addition of a new dictionary should succeed, evicting only the
    // oldest one.

    std::string server_hash_d4;
    EXPECT_TRUE(
        CreateAndAddDictionary(kMaxSizeForTesting / 2, fresh, &server_hash_d4));
    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1));
    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d2));
    EXPECT_FALSE(DictionaryPresentInManager(server_hash_d3));
    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d4));
}

// Confirm using a dictionary changes eviction behavior properly.
TEST_F(SdchOwnerTest, UseChangesEviction)
{
    std::string server_hash_d1;
    std::string server_hash_d2;
    std::string server_hash_d3;

    // Add dictionaries, one recent, one two days in the past, and one
    // four days in the past.
    base::Time fresh(base::Time::Now() - base::TimeDelta::FromHours(23));
    base::Time stale_newer(base::Time::Now() - base::TimeDelta::FromHours(47));
    base::Time stale_older(base::Time::Now() - base::TimeDelta::FromHours(71));

    EXPECT_TRUE(
        CreateAndAddDictionary(kMaxSizeForTesting / 4, fresh, &server_hash_d1));

    EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 4, stale_newer,
        &server_hash_d2));

    EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 4, stale_older,
        &server_hash_d3));

    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1));
    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d2));
    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d3));

    // Use the oldest dictionary.
    sdch_owner().OnDictionaryUsed(server_hash_d3);

    // The addition of a new dictionary should succeed, evicting only the
    // newer stale one.
    std::string server_hash_d4;
    EXPECT_TRUE(
        CreateAndAddDictionary(kMaxSizeForTesting / 2, fresh, &server_hash_d4));
    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1));
    EXPECT_FALSE(DictionaryPresentInManager(server_hash_d2));
    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d3));
    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d4));
}

// Confirm using a dictionary can prevent the addition of a new dictionary.
TEST_F(SdchOwnerTest, UsePreventsAddition)
{
    std::string server_hash_d1;
    std::string server_hash_d2;
    std::string server_hash_d3;

    // Add dictionaries, one recent, one two days in the past, and one
    // four days in the past.
    base::Time fresh(base::Time::Now() - base::TimeDelta::FromMinutes(30));
    base::Time stale_newer(base::Time::Now() - base::TimeDelta::FromHours(47));
    base::Time stale_older(base::Time::Now() - base::TimeDelta::FromHours(71));

    EXPECT_TRUE(
        CreateAndAddDictionary(kMaxSizeForTesting / 4, fresh, &server_hash_d1));

    EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 4, stale_newer,
        &server_hash_d2));

    EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 4, stale_older,
        &server_hash_d3));

    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1));
    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d2));
    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d3));

    // Use the older dictionaries.
    sdch_owner().OnDictionaryUsed(server_hash_d2);
    sdch_owner().OnDictionaryUsed(server_hash_d3);

    // The addition of a new dictionary should fail, not evicting anything.
    std::string server_hash_d4;
    EXPECT_FALSE(
        CreateAndAddDictionary(kMaxSizeForTesting / 2, fresh, &server_hash_d4));
    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1));
    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d2));
    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d3));
    EXPECT_FALSE(DictionaryPresentInManager(server_hash_d4));
}

// Confirm clear gets all the space back.
TEST_F(SdchOwnerTest, ClearReturnsSpace)
{
    std::string server_hash_d1;
    std::string server_hash_d2;

    // Take up all the space.
    EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting, base::Time::Now(),
        &server_hash_d1));
    // Addition should fail.
    EXPECT_FALSE(CreateAndAddDictionary(kMaxSizeForTesting, base::Time::Now(),
        &server_hash_d2));
    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1));
    EXPECT_FALSE(DictionaryPresentInManager(server_hash_d2));
    sdch_manager().ClearData();
    EXPECT_FALSE(DictionaryPresentInManager(server_hash_d1));
    EXPECT_FALSE(DictionaryPresentInManager(server_hash_d2));

    // Addition should now succeed.
    EXPECT_TRUE(
        CreateAndAddDictionary(kMaxSizeForTesting, base::Time::Now(), nullptr));
}

// Confirm memory pressure gets all the space back.
TEST_F(SdchOwnerTest, MemoryPressureReturnsSpace)
{
    std::string server_hash_d1;
    std::string server_hash_d2;

    // Take up all the space.
    EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting, base::Time::Now(),
        &server_hash_d1));

    // Addition should fail.
    EXPECT_FALSE(CreateAndAddDictionary(kMaxSizeForTesting, base::Time::Now(),
        &server_hash_d2));

    EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1));
    EXPECT_FALSE(DictionaryPresentInManager(server_hash_d2));

    base::MemoryPressureListener::NotifyMemoryPressure(
        base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE);
    // The notification may have (implementation note: does :-}) use a PostTask,
    // so we drain the local message queue.  This should be safe (i.e. not have
    // an inifinite number of messages) in a unit test.
    base::RunLoop().RunUntilIdle();

    EXPECT_FALSE(DictionaryPresentInManager(server_hash_d1));
    EXPECT_FALSE(DictionaryPresentInManager(server_hash_d2));

    // Addition should now succeed.
    EXPECT_TRUE(
        CreateAndAddDictionary(kMaxSizeForTesting, base::Time::Now(), nullptr));
}

// Confirm that use of a pinned dictionary after its removal works properly.
TEST_F(SdchOwnerTest, PinRemoveUse)
{
    // Pass ownership of the storage to the SdchOwner, but keep a pointer.
    TestPrefStorage* pref_store = new TestPrefStorage(true);
    sdch_owner().EnablePersistentStorage(
        std::unique_ptr<SdchOwner::PrefStorage>(pref_store));

    std::string server_hash_d1;
    EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 2, base::Time::Now(),
        &server_hash_d1));

    std::unique_ptr<SdchManager::DictionarySet> return_set(
        sdch_manager().GetDictionarySet(
            GURL(std::string(generic_url) + "/x.html")));
    ASSERT_TRUE(return_set.get());
    EXPECT_TRUE(return_set->GetDictionaryText(server_hash_d1));

    const base::Value* result = nullptr;
    const base::DictionaryValue* dict_result = nullptr;
    ASSERT_TRUE(pref_store->GetValue(&dict_result));
    EXPECT_TRUE(dict_result->Get("dictionaries", &result));
    EXPECT_TRUE(dict_result->Get("dictionaries." + server_hash_d1, &result));

    sdch_manager().ClearData();

    ASSERT_TRUE(pref_store->GetValue(&dict_result));
    EXPECT_TRUE(dict_result->Get("dictionaries", &result));
    EXPECT_FALSE(dict_result->Get("dictionaries." + server_hash_d1, &result));

    std::unique_ptr<SdchManager::DictionarySet> return_set2(
        sdch_manager().GetDictionarySet(
            GURL(std::string(generic_url) + "/x.html")));
    EXPECT_FALSE(return_set2.get());

    sdch_manager().OnDictionaryUsed(server_hash_d1);

    ASSERT_TRUE(pref_store->GetValue(&dict_result));
    EXPECT_TRUE(dict_result->Get("dictionaries", &result));
    EXPECT_FALSE(dict_result->Get("dictionaries." + server_hash_d1, &result));
}

TEST_F(SdchOwnerTest, UsageIntervalMetrics)
{
    const GURL url("http://www.example.com/dict0");

    std::string server_hash;
    base::Time last_used_time(base::Time::Now() - base::TimeDelta::FromHours(23));
    base::Time created_time(base::Time::Now() - base::TimeDelta::FromHours(47));

    EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 3, last_used_time,
        created_time, &server_hash));

    base::HistogramTester tester;

    sdch_owner().OnDictionaryUsed(server_hash);
    tester.ExpectTotalCount("Sdch3.FirstUseInterval", 1);
    tester.ExpectTotalCount("Sdch3.UsageInterval2", 0);

    sdch_owner().OnDictionaryUsed(server_hash);
    tester.ExpectTotalCount("Sdch3.FirstUseInterval", 1); // count didn't change
    tester.ExpectTotalCount("Sdch3.UsageInterval2", 1);
}

class SdchOwnerPersistenceTest : public ::testing::Test {
public:
    SdchOwnerPersistenceTest() { }
    virtual ~SdchOwnerPersistenceTest() { }

    void ClearOwner()
    {
        owner_.reset(NULL);
    }

    // If the storage points is non-null it will be saved as the persistent
    // storage for the SdchOwner.
    void ResetOwner(std::unique_ptr<SdchOwner::PrefStorage> storage)
    {
        // This has to be done first, since SdchOwner may be observing SdchManager,
        // and SdchManager can't be destroyed with a live observer.
        owner_.reset(NULL);
        manager_.reset(new SdchManager());
        fetcher_ = new MockSdchDictionaryFetcher();
        owner_.reset(new SdchOwner(manager_.get(),
            &url_request_context_));
        owner_->SetMaxTotalDictionarySize(SdchOwnerTest::kMaxSizeForTesting);
        owner_->SetMinSpaceForDictionaryFetch(
            SdchOwnerTest::kMinFetchSpaceForTesting);
        owner_->SetFetcherForTesting(base::WrapUnique(fetcher_));
        if (storage)
            owner_->EnablePersistentStorage(std::move(storage));
    }

    void InsertDictionaryForURL(const GURL& url, const std::string& nonce)
    {
        owner_->OnDictionaryFetched(base::Time::Now(), base::Time::Now(), 1,
            CreateDictionary(url, nonce), url, net_log_,
            false);
    }

    bool CompleteLoadFromURL(const GURL& url, const std::string& nonce,
        bool was_from_cache)
    {
        return fetcher_->CompletePendingRequest(url, CreateDictionary(url, nonce),
            net_log_, was_from_cache);
    }

    std::string CreateDictionary(const GURL& url, const std::string& nonce)
    {
        std::string dict;
        dict.append("Domain: ");
        dict.append(url.host());
        dict.append("\n\n");
        dict.append(url.spec());
        dict.append(nonce);
        return dict;
    }

protected:
    BoundNetLog net_log_;
    std::unique_ptr<SdchManager> manager_;
    MockSdchDictionaryFetcher* fetcher_;
    std::unique_ptr<SdchOwner> owner_;
    TestURLRequestContext url_request_context_;
};

// Test an empty persistence store.
TEST_F(SdchOwnerPersistenceTest, Empty)
{
    ResetOwner(base::WrapUnique(new TestPrefStorage(true)));
    EXPECT_EQ(0, owner_->GetDictionaryCountForTesting());
}

// Test a persistence store with a bad version number.
TEST_F(SdchOwnerPersistenceTest, Persistent_BadVersion)
{
    std::unique_ptr<base::DictionaryValue> sdch_dict(new base::DictionaryValue());
    sdch_dict->SetInteger("version", 2);

    std::unique_ptr<TestPrefStorage> storage(new TestPrefStorage(true));
    storage->SetValue(std::move(sdch_dict));

    TestPrefStorage* old_storage = storage.get(); // Save storage pointer.
    ResetOwner(std::move(storage)); // Takes ownership of storage pointer.

    storage.reset(new TestPrefStorage(*old_storage));
    ResetOwner(std::move(storage));
    EXPECT_EQ(0, owner_->GetDictionaryCountForTesting());
}

// Test a persistence store with an empty dictionaries map.
TEST_F(SdchOwnerPersistenceTest, Persistent_EmptyDictList)
{
    std::unique_ptr<base::DictionaryValue> sdch_dict(new base::DictionaryValue());
    std::unique_ptr<base::DictionaryValue> dicts(new base::DictionaryValue());
    sdch_dict->SetInteger("version", 1);
    sdch_dict->Set("dictionaries", std::move(dicts));

    std::unique_ptr<TestPrefStorage> storage(new TestPrefStorage(true));
    storage->SetValue(std::move(sdch_dict));
    ResetOwner(std::move(storage));
    EXPECT_EQ(0, owner_->GetDictionaryCountForTesting());
}

TEST_F(SdchOwnerPersistenceTest, OneDict)
{
    const GURL url("http://www.example.com/dict");

    std::unique_ptr<TestPrefStorage> storage(new TestPrefStorage(true));
    TestPrefStorage* old_storage = storage.get(); // Save storage pointer.
    ResetOwner(std::move(storage)); // Takes ownership of storage pointer.
    EXPECT_EQ(0, owner_->GetDictionaryCountForTesting());
    InsertDictionaryForURL(url, "0");
    EXPECT_EQ(1, owner_->GetDictionaryCountForTesting());

    storage.reset(new TestPrefStorage(*old_storage));
    ResetOwner(std::move(storage));
    EXPECT_EQ(0, owner_->GetDictionaryCountForTesting());
    EXPECT_TRUE(CompleteLoadFromURL(url, "0", true));
    EXPECT_EQ(1, owner_->GetDictionaryCountForTesting());
}

TEST_F(SdchOwnerPersistenceTest, TwoDicts)
{
    const GURL url0("http://www.example.com/dict0");
    const GURL url1("http://www.example.com/dict1");

    std::unique_ptr<TestPrefStorage> storage(new TestPrefStorage(true));
    TestPrefStorage* old_storage = storage.get(); // Save storage pointer.
    ResetOwner(std::move(storage)); // Takes ownership of storage pointer.
    InsertDictionaryForURL(url0, "0");
    InsertDictionaryForURL(url1, "1");

    storage.reset(new TestPrefStorage(*old_storage));
    ResetOwner(std::move(storage));
    EXPECT_TRUE(CompleteLoadFromURL(url0, "0", true));
    EXPECT_TRUE(CompleteLoadFromURL(url1, "1", true));
    EXPECT_EQ(2, owner_->GetDictionaryCountForTesting());
    EXPECT_TRUE(owner_->HasDictionaryFromURLForTesting(url0));
    EXPECT_TRUE(owner_->HasDictionaryFromURLForTesting(url1));
}

TEST_F(SdchOwnerPersistenceTest, OneGoodDictOneBadDict)
{
    const GURL url0("http://www.example.com/dict0");
    const GURL url1("http://www.example.com/dict1");

    std::unique_ptr<TestPrefStorage> storage(new TestPrefStorage(true));
    TestPrefStorage* old_storage = storage.get(); // Save storage pointer.
    ResetOwner(std::move(storage)); // Takes ownership of storage.
    InsertDictionaryForURL(url0, "0");
    InsertDictionaryForURL(url1, "1");

    // Make a new storage based on the current contents of the old one.
    storage.reset(new TestPrefStorage(*old_storage));
    base::DictionaryValue* dict = nullptr;
    ASSERT_TRUE(GetDictionaryForURL(storage.get(), url1, nullptr, &dict));
    dict->Remove("use_count", nullptr);

    ResetOwner(std::move(storage));
    EXPECT_TRUE(CompleteLoadFromURL(url0, "0", true));
    EXPECT_FALSE(CompleteLoadFromURL(url1, "1", true));
    EXPECT_EQ(1, owner_->GetDictionaryCountForTesting());
    EXPECT_TRUE(owner_->HasDictionaryFromURLForTesting(url0));
    EXPECT_FALSE(owner_->HasDictionaryFromURLForTesting(url1));
}

TEST_F(SdchOwnerPersistenceTest, UsingDictionaryUpdatesUseCount)
{
    const GURL url("http://www.example.com/dict");

    std::unique_ptr<TestPrefStorage> storage(new TestPrefStorage(true));
    TestPrefStorage* old_storage = storage.get(); // Save storage pointer.
    ResetOwner(std::move(storage)); // Takes ownership of storage pointer.
    InsertDictionaryForURL(url, "0");

    std::string hash;
    int old_count;
    storage.reset(new TestPrefStorage(*old_storage));
    {
        ClearOwner();
        base::DictionaryValue* dict = nullptr;
        ASSERT_TRUE(GetDictionaryForURL(storage.get(), url, &hash, &dict));
        ASSERT_TRUE(dict->GetInteger("use_count", &old_count));
    }

    old_storage = storage.get(); // Save storage pointer.
    ResetOwner(std::move(storage)); // Takes ownership of storage pointer.
    ASSERT_TRUE(CompleteLoadFromURL(url, "0", true));
    owner_->OnDictionaryUsed(hash);

    int new_count;
    {
        base::DictionaryValue* dict = nullptr;
        ASSERT_TRUE(GetDictionaryForURL(old_storage, url, nullptr, &dict));
        ASSERT_TRUE(dict->GetInteger("use_count", &new_count));
    }

    EXPECT_EQ(old_count + 1, new_count);
}

TEST_F(SdchOwnerPersistenceTest, LoadingDictionaryMerges)
{
    const GURL url0("http://www.example.com/dict0");
    const GURL url1("http://www.example.com/dict1");

    std::unique_ptr<TestPrefStorage> storage(new TestPrefStorage(true));
    TestPrefStorage* old_storage = storage.get(); // Save storage pointer.
    ResetOwner(std::move(storage)); // Takes ownership of storage pointer.
    InsertDictionaryForURL(url1, "1");

    storage.reset(new TestPrefStorage(*old_storage));
    ResetOwner(std::unique_ptr<SdchOwner::PrefStorage>());
    InsertDictionaryForURL(url0, "0");
    EXPECT_EQ(1, owner_->GetDictionaryCountForTesting());
    owner_->EnablePersistentStorage(std::move(storage));
    ASSERT_TRUE(CompleteLoadFromURL(url1, "1", true));
    EXPECT_EQ(2, owner_->GetDictionaryCountForTesting());
}

TEST_F(SdchOwnerPersistenceTest, PersistenceMetrics)
{
    const GURL url0("http://www.example.com/dict0");
    const GURL url1("http://www.example.com/dict1");

    std::unique_ptr<TestPrefStorage> storage(new TestPrefStorage(true));
    TestPrefStorage* old_storage = storage.get(); // Save storage pointer.
    ResetOwner(std::move(storage)); // Takes ownership of storage pointer.

    InsertDictionaryForURL(url0, "0");
    InsertDictionaryForURL(url1, "1");

    storage.reset(new TestPrefStorage(*old_storage));
    ResetOwner(std::move(storage));

    base::HistogramTester tester;

    EXPECT_TRUE(CompleteLoadFromURL(url0, "0", true));
    EXPECT_TRUE(CompleteLoadFromURL(url1, "1", false));

    tester.ExpectTotalCount("Sdch3.NetworkBytesSpent", 1);
    tester.ExpectUniqueSample("Sdch3.NetworkBytesSpent",
        CreateDictionary(url1, "1").size(), 1);
}

} // namespace net
